package com.hqd.ch03.v27.spel.standard;

import com.hqd.ch03.v27.spel.exception.InternalParseException;
import com.hqd.ch03.v27.spel.exception.SpelMessage;
import com.hqd.ch03.v27.spel.exception.SpelParseException;

import java.util.ArrayList;
import java.util.List;

/**
 * 词法解析：将获取到的表达式解析成Token
 */
public class Tokenizer {
    private String expressionString;
    private char[] charsToProcess;
    private int pos;
    private int max;
    private List<Token> tokens = new ArrayList<>();

    public Tokenizer(String expressionString) {
        this.expressionString = expressionString;
        this.charsToProcess = this.expressionString.toCharArray();
        this.pos = 0;
        this.max = this.expressionString.length();
    }


    /**
     * 词法解析
     *
     * @return
     */
    public List<Token> process() {
        while (this.pos < this.max) {
            char ch = this.charsToProcess[pos++];
            if (Character.isLetter(ch)) {
                lexIdentifier(ch);
            } else {
                switch (ch) {
                    case '0':
                    case '1':
                    case '2':
                    case '3':
                    case '4':
                    case '5':
                    case '6':
                    case '7':
                    case '8':
                    case '9':
                        pushNumberToken(ch);
                        break;
                    case '+':
                        pushToken(TokenKind.PLUS);
                        break;
                    case '-':
                        pushToken(TokenKind.MINUS);
                        break;
                    case '*':
                        pushToken(TokenKind.STAR);
                        break;
                    case '/':
                        pushToken(TokenKind.DIV);
                        break;
                    case '%':
                        pushToken(TokenKind.MOD);
                        break;
                    case '(':
                        pushToken(TokenKind.LPAREN);
                        break;
                    case ')':
                        pushToken(TokenKind.RPAREN);
                        break;
                    case '.':
                        pushToken(TokenKind.DOT);
                        break;
                    case ',':
                        pushToken(TokenKind.COMMA);
                        break;
                    case '\'':
                        lexQuotedStringLiteral();
                        break;
                    case '=':
                        if (isTwoCharToken(TokenKind.EQ)) {
                            pushPairToken(TokenKind.EQ);
                        } else {
                            pushToken(TokenKind.ASSIGN);
                        }
                        break;
                    case '<':
                        if (isTwoCharToken(TokenKind.LE)) {
                            pushPairToken(TokenKind.LE);
                        } else {
                            pushToken(TokenKind.LT);
                        }
                        break;
                    case '>':
                        if (isTwoCharToken(TokenKind.GE)) {
                            pushPairToken(TokenKind.GE);
                        } else {
                            pushToken(TokenKind.GT);
                        }
                        break;
                    case '&':
                        if (isTwoCharToken(TokenKind.SYMBOLIC_AND)) {
                            pushPairToken(TokenKind.SYMBOLIC_AND);
                        } else {
                            raiseParseException(pos, SpelMessage.MISSING_CHARACTER, "&");
                        }
                        break;
                    case '|':
                        if (!isTwoCharToken(TokenKind.SYMBOLIC_OR)) {
                            raiseParseException(this.pos, SpelMessage.MISSING_CHARACTER, "|");
                        }
                        pushPairToken(TokenKind.SYMBOLIC_OR);
                        break;
                    case '!':
                        if (isTwoCharToken(TokenKind.NE)) {
                            pushPairToken(TokenKind.NE);
                        } else {
                            pushToken(TokenKind.NOT);
                        }
                        break;
                    default:
                        break;
                }
            }
        }
        return tokens;
    }

    private void lexIdentifier(char ch) {
        int start = this.pos - 1;
        StringBuilder sb = new StringBuilder();
        sb.append(ch);
        while (pos < max) {
            ch = this.charsToProcess[this.pos++];
            if (isIdentifier(ch)) {
                sb.append(ch);
            } else {
                this.pos--;
                break;
            }
        }
        this.tokens.add(new Token(TokenKind.IDENTIFIER, sb.toString().toCharArray(), start, this.pos));
    }

    private boolean isIdentifier(char ch) {
        return Character.isLetter(ch) || Character.isDigit(ch) || ch == '_' || ch == '$';
    }

    private void pushPairToken(TokenKind kind) {
        this.tokens.add(new Token(kind, this.pos - 1, this.pos + 1));
        this.pos++;
    }

    private void pushToken(TokenKind kind) {
        pushToken(kind, this.pos - 1, this.pos);
    }

    private void pushToken(TokenKind kind, int startPos, int endPos) {
        tokens.add(new Token(kind, startPos, endPos));
    }

    private void lexQuotedStringLiteral() {
        int start = pos - 1;
        StringBuilder sb = new StringBuilder();
        while (pos < max) {
            char ch = charsToProcess[pos++];
            if (ch != '\'') {
                sb.append(ch);
            } else {
                break;
            }
        }
        int end = this.pos - 1;
        tokens.add(new Token(TokenKind.LITERAL_STRING, sb.toString().toCharArray(), start, end));
    }

    private void pushNumberToken(char ch) {
        StringBuilder sb = new StringBuilder();
        sb.append(ch);
        TokenKind kind = TokenKind.LITERAL_INT;
        int start = this.pos - 1;
        sb.append(getNumber());
        if (pos < max) {
            ch = charsToProcess[pos];
            if (ch == '.') {
                sb.append(ch);
                pos++;
                sb.append(getNumber());
                kind = TokenKind.LITERAL_REAL;
            }
        }
        int end = this.pos - 1;
        tokens.add(new Token(kind, sb.toString().toCharArray(), start, end));
    }

    private String getNumber() {
        StringBuilder sb = new StringBuilder();
        while (pos < max) {
            char ch = charsToProcess[this.pos++];
            if (ch >= '0' && ch <= '9') {
                sb.append(ch);
            } else {
                this.pos--;
                break;
            }
        }
        return sb.toString();
    }

    private boolean isTwoCharToken(TokenKind kind) {
        return (kind.tokenChars.length == 2 &&
                this.charsToProcess[this.pos - 1] == kind.tokenChars[0] &&
                this.charsToProcess[this.pos] == kind.tokenChars[1]);
    }

    private void raiseParseException(int start, SpelMessage msg, Object... inserts) {
        throw new InternalParseException(new SpelParseException(this.expressionString, start, msg, inserts));
    }

}
